package org.spruice;

import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;
import org.spruice.proxy.ProxyCreator;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

public class BinderImpl implements Binder {

    private static final AtomicInteger uid = new AtomicInteger();
    private final List<ContextEntry> builders = new ArrayList<ContextEntry>();
    private final ProxyCreator proxy = new ProxyCreator();
    private final AutoNamer autoNamer;
    private final List<Runnable> startable = new ArrayList<Runnable>();
    private final List<Runnable> stoppable = new ArrayList<Runnable>();
    private final List<Runnable> destroyable = new ArrayList<Runnable>();
    private boolean errorOnAmbiguousAutowire = false;

    public BinderImpl() {
        this(new CamelCaseNamer());
    }

    public void setErrorOnAmbiguousAutowire(boolean errorOnAmbiguousAutowire) {
        this.errorOnAmbiguousAutowire = errorOnAmbiguousAutowire;
    }

    public BinderImpl(AutoNamer namer) {
        this.autoNamer = namer;
        BeanConfig<BinderLifecycle> life = construct("BinderLifecycle" + uid.getAndIncrement(), BinderLifecycle.class);
        life.set().setOnStart(startable);
        life.set().setOnStop(stoppable);
        life.set().setOnDestroy(destroyable);
        life.destroyWith().destroy();
    }

    public <T> BeanConfig<T> autowire(Class<T> clzz) {
        return autowire(autoNamer.next(clzz), clzz);
    }

    public <T> BeanConfig<T> autowire(String name, Class<T> clzz) {
        if(errorOnAmbiguousAutowire){
            checkAmbiguous(clzz);
        }
        BeanDefinitionBuilder builder = createBeanBuilder(clzz);
        builder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
        BeanConfig<T> config = new BeanConfig<T>(name, builder, proxy, clzz);
        builders.add(config);
        return config;
    }

    private static class CKey {
        private final int argLength;
        private final boolean isPublic;

        public CKey(Constructor c){
            isPublic = Modifier.isPublic(c.getModifiers());
            argLength = c.getParameterTypes().length;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            CKey cKey = (CKey) o;

            if (argLength != cKey.argLength) return false;
            if (isPublic != cKey.isPublic) return false;
            return true;
        }

        @Override
        public int hashCode() {
            int result = argLength;
            result = 31 * result + (isPublic ? 1 : 0);
            return result;
        }
    }

    private <T> void checkAmbiguous(Class<T> clzz) {
        Map<CKey, Constructor> cs = new HashMap<CKey, Constructor>();
        Constructor<?>[] declaredConstructors = clzz.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            Constructor put = cs.put(new CKey(declaredConstructor), declaredConstructor);
            if(put != null){
                throw new RuntimeException("Ambiguous Constructors: " + put + " : " + declaredConstructor +". Spring cannot differentiate constructors with the same number of arguments.");
            }
        }

    }

    public <T> BeanConfig<T> construct(Class<T> clzz, Object... args) {
        return construct(autoNamer.next(clzz), clzz, args);
    }

    public <T> BeanConfig<T> construct(String name, Class<T> clzz, Object... args) {
        BeanDefinitionBuilder builder = createBeanBuilder(clzz);
        addConstructorArgs(builder, args);
        BeanConfig<T> config = new BeanConfig<T>(name, builder, proxy, clzz);
        builders.add(config);
        return config;
    }

    private void addConstructorArgs(BeanDefinitionBuilder builder, Object... args) {
        for (Object arg : args) {
            String beanName = BeanConfig.resolveBeanNameFromObject(arg);
            if (beanName != null) {
                builder.addConstructorArgReference(beanName);
            } else {
                builder.addConstructorArgValue(arg);
            }
        }
    }

    public <T> InstanceConfig<T> register(final T singleton) {
        return register(autoNamer.next(singleton.getClass()), singleton);
    }

    public <T> InstanceConfig<T> register(final String name, final T singleton) {
        ContextEntry entry = new ContextEntry() {
            public void addTo(GenericApplicationContext context) {
                context.getBeanFactory().registerSingleton(name, singleton);
            }
        };
        builders.add(entry);
        return new InstanceConfig<T>(name, singleton, this.proxy, startable, stoppable, destroyable);
    }

    public <T, R> FactoryConfig<T, R> asFactory(BeanConfig<T> factoryBean, Class<R> targetClzz) {
        return new FactoryConfig<T, R>(factoryBean, this, targetClzz);
    }

    public <T> BeanDefinitionBuilder createBeanBuilder(Class<T> clzz) {
        if (clzz == null) {
            return BeanDefinitionBuilder.genericBeanDefinition();
        }
        return BeanDefinitionBuilder.genericBeanDefinition(clzz);
    }

    public List<ContextEntry> getConfigs() {
        return builders;
    }

    public <T> BeanConfig<T> factory(String name, String factoryBean, String method, Class<T> clzz, Object... args) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clzz);
        builder.setFactoryMethod(method);
        builder.getRawBeanDefinition().setFactoryBeanName(factoryBean);
        addConstructorArgs(builder, args);
        BeanConfig<T> config = new BeanConfig<T>(name, builder, proxy, clzz);
        builders.add(config);
        return config;
    }
}
